/*
 path_render.cpp
 Matthew Jee
 mcjee@ucsc.edu
*/

#include <iostream>
#include <cmath>
#include <cstdlib>
#include "../renderer/definitions.h"
#include "../renderer/scene.h"
#include "meshgeneration.h"
#include "io.h"

using namespace std;

mj::Scene *_scene;
mj::Texture2D *_renderBufferTexture;
mj::Shader *_clusterShader;
mj::Shader *_displayShader;
mj::VertexAttributeSetup *_coloredVertexSetup;

vector<mj::Path> _paths;

/* General Settings */

GLfloat _aspectRatio;
GLfloat _sceneSize;

GLfloat _zoomSpeed = 0.02;
GLfloat _zoomMin   = 0.2;
GLfloat _zoomMax   = 20.0;
GLfloat _zoom      = 1.0;

bool _panning;
double _xPrev, _yPrev;

/* Rendering */
#pragma mark -
#pragma mark Rendering

void compileShaders() {
    _clusterShader = new mj::Shader();
    _displayShader = new mj::Shader();
    try {
        _clusterShader->compile("shaders/distribution.vsh", "shaders/distribution.fsh");
        _displayShader->compile("shaders/display.vsh", "shaders/display.fsh");
    } catch(exception &e) {
        cerr << e.what() << endl;
        glfwTerminate();
        exit(EXIT_FAILURE);
    }
}

void addPaths(vector<mj::Path> &paths, mj::Vector4 color=mj::Vector4(-1.0, -1.0, -1.0, -1.0)) {
    mj::Mesh *pathMesh = mj::generatePathMesh(paths, color);
    mj::Renderable *pathRenderable = new mj::Renderable(pathMesh, _clusterShader, GL_LINES);
    pathRenderable->setupVertexAttributes = _coloredVertexSetup;
    pathRenderable->init();
    _scene->add(pathRenderable);
}

void setupScene() {
    _scene = new mj::Scene(NULL);
    _scene->blendEnabled = true;
    _scene->camera.transform.position = mj::Vector3(0.0, 0.0, -10.0);

    cerr << "generating mesh" << endl;

    _coloredVertexSetup = new mj::VertexAttributeSetup();
    _coloredVertexSetup->addAttribute("vPosition", 3, GL_FLOAT, GL_FALSE, 7*sizeof(GLfloat), 0);
    _coloredVertexSetup->addAttribute("vColor", 4, GL_FLOAT, GL_FALSE, 7*sizeof(GLfloat), (GLvoid *)(3*sizeof(GLfloat)));
}

/* GLFW Callbacks */
#pragma mark -
#pragma mark GLFW Callbacks

void errorCallback(int error, const char *description) {
    cerr << "GLFW Error " << error << " : " << description;
}

void frameBufferSizeCallback(GLFWwindow *window, int width, int height) {
    _aspectRatio = 1.0*width/height;

    if (_renderBufferTexture) {
        delete _renderBufferTexture;
    }
    _renderBufferTexture = new mj::Texture2D(GL_RGBA, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, width, height);
    _renderBufferTexture->interpolation(GL_LINEAR);
    _renderBufferTexture->borderColor(mj::Vector4(1.0, 1.0, 0.0, 1.0));
    _renderBufferTexture->initData((float *)0);

    if (_scene->framebuffer) {
        delete _scene->framebuffer;
    }
    mj::Framebuffer *framebuffer = new mj::Framebuffer(width, height);
    framebuffer->addRenderTarget(_renderBufferTexture, GL_COLOR_ATTACHMENT0);
    framebuffer->addRenderTarget(GL_DEPTH_COMPONENT, GL_DEPTH_ATTACHMENT);
    framebuffer->backgroundColor = mj::Vector4(1.0, 1.0, 1.0, 1.0);
    framebuffer->clear(GL_COLOR_BUFFER_BIT);

    _scene->framebuffer = framebuffer;
    _scene->camera.orthographic(-_sceneSize*_aspectRatio, _sceneSize*_aspectRatio, -_sceneSize, _sceneSize, 2.0f, 60.0f);

    glViewport(0, 0, width, height);
    _displayShader->use();
    _displayShader->setUniform1f("aspectRatio", 1);
}

void scrollCallback(GLFWwindow *window, double x, double y) {
    _zoom *= 1.0+y*_zoomSpeed;
    if (_zoom > _zoomMax) {
        _zoom = _zoomMax;
    } else if (_zoom < _zoomMin) {
        _zoom = _zoomMin;
    }
    _sceneSize = 140.0/_zoom;
    _scene->camera.orthographic(-_sceneSize*_aspectRatio, _sceneSize*_aspectRatio, -_sceneSize, _sceneSize, 2.0f, 60.0f);
}

void mouseButtonCallback(GLFWwindow *window, int button, int action, int mods) {
    if (action == GLFW_PRESS && button == GLFW_MOUSE_BUTTON_LEFT) {
        _panning = true;
        glfwGetCursorPos(window, &_xPrev, &_yPrev);
    } else if (action == GLFW_RELEASE && button == GLFW_MOUSE_BUTTON_LEFT) {
        _panning = false;
    }
}

void mouseMoveCallback(GLFWwindow *window, double x, double y) {

    int width, height;
    glfwGetWindowSize(window, &width, &height);
    double xScale = (_sceneSize * _aspectRatio * 2.0) / width;
    double yScale = (_sceneSize * 2.0) / height;

    if (_panning) {
        double xDiff = x-_xPrev;
        double yDiff = y-_yPrev;
        _scene->camera.transform.position.x -= xDiff * xScale;
        _scene->camera.transform.position.y += yDiff * yScale;
    }
    _xPrev = x;
    _yPrev = y;
}

/* Main */
#pragma mark -
#pragma mark Main

int main(int argc, char **argv) {

    if (argc < 1) {
        cerr << "usage: path_render [other_paths]" << endl;
        return 1;
    }

    _sceneSize = 140.0f;

    glfwSetErrorCallback(errorCallback);

    if (!glfwInit()) {
        glfwTerminate();
        exit(EXIT_FAILURE);
    }

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow *window = glfwCreateWindow(800, 600, "Path Render", NULL, NULL);
    if (!window) {
        glfwTerminate();
        exit(EXIT_FAILURE);
    }
    glfwMakeContextCurrent(window);
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);

    glfwSetFramebufferSizeCallback(window, frameBufferSizeCallback);
    glfwSetScrollCallback(window, scrollCallback);
    glfwSetMouseButtonCallback(window, mouseButtonCallback);
    glfwSetCursorPosCallback(window, mouseMoveCallback);
    
    // load paths
    vector<mj::Path> paths = readTraclusPaths(cin);

    compileShaders();
    setupScene();
    addPaths(paths);

    if (argc == 2) {
        ifstream otherPathFile(argv[1], ifstream::in);
        vector<mj::Path> otherPaths = readTraclusPaths(otherPathFile);
        addPaths(otherPaths, mj::Vector4(0.0, 0.0, 0.0, 0.2));
    }

    frameBufferSizeCallback(NULL, 800, 600);

    glEnable(GL_LINE_SMOOTH);
    while (!glfwWindowShouldClose(window)) {
        _scene->render();

        _renderBufferTexture->present(_displayShader);
        glfwSwapBuffers(window);
        glfwWaitEvents();
    }

    glfwDestroyWindow(window);
    glfwTerminate();
    return 0;
}
